feat(desktop): owner-global observer ingestion at app level#1493
Conversation
f5edf42 to
f9ffb01
Compare
Rebased from tho/observer-owner-global (#1493) onto post-#1380 main. Make agent observer ingestion (kind 24200 frame decryption + derived active-turn liveness) a single app-level concern instead of per-surface bridge mounts: - useAgentObserverIngestion mounted once in AppShell registers all locally managed agents plus relay agents the identity declared-owns (NIP-OA ownerPubkey via one batched profile query, treated deployed) - Pure combineObserverIngestionAgents() with unit coverage - Remove per-surface bridge mounts: ChannelScreen, UserProfilePanel, useManagedAgentActions, useActiveWorkingChannelsById, usePreventSleep Conflict note vs #1380: UserProfilePanel keeps #1380's activityAgent memo (still feeds the profile activity preview); only its bridge mounts (activityBridgeAgents) are removed since app-level ingestion covers both managed and declared-owned relay agents. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
c44c50b to
a7c6b39
Compare
f9ffb01 to
22feb20
Compare
Rebased from tho/observer-owner-global (#1493) onto post-#1380 main. Make agent observer ingestion (kind 24200 frame decryption + derived active-turn liveness) a single app-level concern instead of per-surface bridge mounts: - useAgentObserverIngestion mounted once in AppShell registers all locally managed agents plus relay agents the identity declared-owns (NIP-OA ownerPubkey via one batched profile query, treated deployed) - Pure combineObserverIngestionAgents() with unit coverage - Remove per-surface bridge mounts: ChannelScreen, UserProfilePanel, useManagedAgentActions, useActiveWorkingChannelsById, usePreventSleep Conflict note vs #1380: UserProfilePanel keeps #1380's activityAgent memo (still feeds the profile activity preview); only its bridge mounts (activityBridgeAgents) are removed since app-level ingestion covers both managed and declared-owned relay agents. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
a7c6b39 to
be5994c
Compare
Rebased from tho/observer-owner-global (#1493) onto post-#1380 main. Make agent observer ingestion (kind 24200 frame decryption + derived active-turn liveness) a single app-level concern instead of per-surface bridge mounts: - useAgentObserverIngestion mounted once in AppShell registers all locally managed agents plus relay agents the identity declared-owns (NIP-OA ownerPubkey via one batched profile query, treated deployed) - Pure combineObserverIngestionAgents() with unit coverage - Remove per-surface bridge mounts: ChannelScreen, UserProfilePanel, useManagedAgentActions, useActiveWorkingChannelsById, usePreventSleep Conflict note vs #1380: UserProfilePanel keeps #1380's activityAgent memo (still feeds the profile activity preview); only its bridge mounts (activityBridgeAgents) are removed since app-level ingestion covers both managed and declared-owned relay agents. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
22feb20 to
8d0becf
Compare
be5994c to
ec29b2e
Compare
| // Owner-global observer ingestion: receives + decrypts agent observer | ||
| // frames and keeps derived active-turn liveness in sync app-wide, so no | ||
| // individual screen/panel has to mount its own bridge for ingestion. | ||
| useAgentObserverIngestion(); |
There was a problem hiding this comment.
🟡 [IMPORTANT · doc-clarity] useAgentObserverIngestion mounts before identity resolves — deferral is silent.
This is called unconditionally, without the startupReady/identity guard that the other identity-dependent hooks in this block use. When currentPubkey is undefined (pre-resolve), combineObserverIngestionAgents early-returns managed-agents-only, so relay-owned agents aren't included until a second render after identity resolves.
This is safe — the early-return handles it correctly, no data loss — but the deferral is invisible to a reader. Worth a one-line inline/jsdoc note that relay-owned inclusion is intentionally deferred until identity resolves, so nobody "fixes" it by adding a guard that breaks the managed-agent coverage during startup.
| const ownerByPubkey = new Map<string, string>(); | ||
| for (const [pubkey, summary] of Object.entries(profiles ?? {})) { | ||
| if (summary.ownerPubkey) { | ||
| ownerByPubkey.set(normalizePubkey(pubkey), summary.ownerPubkey); |
There was a problem hiding this comment.
🟡 [IMPORTANT · doc-clarity] ownerByPubkey key is normalized but value is stored raw.
Here the key is normalizePubkey(pubkey) but the value is the raw summary.ownerPubkey. In combineObserverIngestionAgents the value is normalized at compare time (normalizePubkey(owner) === me), so this is correct end-to-end.
But the asymmetry (normalized key, raw value, normalize-at-compare) is a read trap, and the unit test only feeds pre-normalized 64-char lowercase keys — so the raw-value path is never exercised. Suggest either storing normalizePubkey(summary.ownerPubkey) as the value, or adding a test case with a mixed-case ownerPubkey to document the intent. Not a bug — a future footgun.
…value Two review follow-ups on owner-global observer ingestion: - Document (AppShell + hook jsdoc) that useAgentObserverIngestion intentionally mounts before identity resolves: managed agents are ingested immediately, relay-owned agents join once currentPubkey arrives. Guards against a future identity/startup gate that would drop managed-agent coverage during startup. - Store ownerByPubkey values normalized (was: normalized key, raw value, normalize-at-compare) so lookups and ownership comparisons never depend on relay casing. Behavior unchanged; removes the asymmetry footgun. Addresses review feedback on #1493. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…1494) Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
Overview
Category: improvement
User Impact: Agent activity and working indicators (sidebar badges, profile badges, activity panels) now update reliably no matter which screen is open — including for remote agents you own but don't run locally.
Problem: Observer ingestion was mount-timing incidental: frames were only received, decrypted, and folded into derived liveness state while some surface happened to mount its own bridge with the right agent list. That made indicators brittle and inconsistent — a panel could show raw turn events while another surface's liveness stayed stale (the bug class fixed point-wise in #1492), and remote owned agents were only registered when specific panels were open.
Solution: Ingestion becomes a single app-level concern. A new
useAgentObserverIngestionhook mounts once inAppShell, registers all locally managed agents plus relay agents the current identity declared-owns (NIP-OAownerPubkey, resolved via one batched profile query), and feeds both the observer store and the derived active-turns store app-wide. All five per-surface bridge mounts are removed; surfaces now only read from the stores. This establishes the product invariant: if you own an agent, its turn activity is ingested app-wide.File changes
desktop/src/features/agents/useAgentObserverIngestion.ts
New app-level ingestion hook plus pure
combineObserverIngestionAgents(): managed agents keep their real status; declared-owned relay agents not managed locally are added asdeployed. Non-owned agents are excluded — their frames are#p-addressed to their owner and never arrive on our subscription anyway.desktop/src/features/agents/useAgentObserverIngestion.test.mjs
Unit coverage for the combination logic: status passthrough, owned-relay inclusion, foreign/ownerless exclusion, managed/relay dedupe, case-insensitive matching, unresolved-identity fallback.
desktop/src/app/AppShell.tsx
Mounts the ingestion hook alongside the other always-on workspace managers.
desktop/src/features/channels/ui/ChannelScreen.tsx
Removes the per-screen observer/turns bridge mounts and the synthetic
observerBridgeAgentsmemo (profile-panel agents are covered by app-level owned-relay registration).desktop/src/features/profile/ui/UserProfilePanel.tsx
Removes the panel-local bridge mounts and both synthetic agent-list memos — the owned-relay "deployed" seeding it did is now the app-level rule.
desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.ts
Sidebar working-badge hook becomes read-only over the derived store.
desktop/src/features/agents/ui/useManagedAgentActions.ts
Agents-page hook becomes read-only over the derived store.
desktop/src/features/agents/usePreventSleep.ts
Drops its own bridge mount; keeps reading observer snapshots for activity tracking.
Reproduction Steps
ownerPubkey, not managed locally): open its Activity from a profile anywhere — frames decrypt without needing the specific panels that used to seed the known-agents set.cd desktop && pnpm test(1558/1558, 7 new).No visual/markup changes — this is ingestion plumbing consolidation.